<?php

/**
 * Km class
 *
 * @author vbolshov
 */

if (! defined('KM_ROOT'))
{
	define('KM_ROOT', dirname(__FILE__));
}

use km\HttpResponse;
use km\Cache;
use \PDO;
use \Closure;
use km\log\StdoutLogger;

class Km {
	/**
	 * Autoloaded libraries a mapped here
	 *
	 * @var array (LIBRARY-NAME => LIBRARY-ROOT-FOLDER)
	 */
	static private $library_map = array('km' => KM_ROOT);
	/**
	 * @var string
	 */
	static private $default_library_root;
	/**
	 * Autoloaded classes that do not conform to the naming conventions,
	 * could be put here
	 */
	static private $class_map = array();
	/**
	 * @var array (ExceptionClass => handler (Closure),..)
	 */
	static private $exception_handlers = array();
	/**
	 * @var HttpResponse
	 */
	static private $http_response;
	/**
	 * shared variables
	 * @vars array
	 */
	static private $shared = array();
	/**
	 * Configuration
	 * @var array
	 */
	static private $settings = array();
	/**
	 * @var PDO
	 */
	static private $pdo;
	/**
	 * @var PDO
	 */
	static private $transactionDepth = 0;
	/**
	 * @var Cache
	 */
	static private $cache;
	/**
	 * @var Logger
	 */
	static private $logger;
	/**
	 * @param string $name either usual PHP syntax ('ns1\\ns2\\ClassName')
	 * or dot-separated syntax: 'ns1.ns2.ClassName'
	 * @return string ns1\\ns2\\ClassName
	 */
	static function className($name)
	{
		return str_replace('.', '\\', $name);
	}
	/**
	 * Factory method. Create an instance of $class.
	 * Class name may be specified using usual PHP syntax ('ns1\\ns2\\ClassName')
	 * or using dot-separated syntax: 'ns1.ns2.ClassName' - for conviniency.
	 * @param string $class
	 * @param array $constructor_args
	 * @param string $real_name the real class name will be placed here
	 * @return object
	 */
	static function create($class, $constructor_args = null, & $real_name = null)
	{
		$real_name = self::className($class);
		return call_user_func_array(array(new ReflectionClass($real_name), 'newInstance'), (array) $constructor_args);
	}
	/**
	 * @param PDO | Proxy $connection
	 */
	static function setDbConnection($connection)
	{
		self::$pdo = $connection;
	}
	/**
	 * @return PDO | Proxy
	 */
	static function getDbConnection()
	{
		return self::$pdo;
	}
	/**
	 * safely start transaction (supports nested transactions, kinda)
	 */
	static function beginTransaction()
	{
		if (! self::$transactionDepth) {
			self::getDbConnection()->beginTransaction();
		}
		self::$transactionDepth++;
	}
	/**
	 * safely commit transaction (supports nested transactions, kinda)
	 */
	static function commit()
	{
		self::$transactionDepth--;
		if (! self::$transactionDepth) {
			self::getDbConnection()->commit();
		}
	}
	/**
	 * safely rollback transaction (supports nested transactions, kinda)
	 */
	static function rollback()
	{
		if (self::$transactionDepth) {
			self::$transactionDepth = 0;
			self::getDbConnection()->rollback();
		}
	}
	/**
	 * @param Cache $driver
	 */
	static function setCacheDriver($driver)
	{
		self::$cache = $driver;
	}
	/**
	 * @return Cache
	 */
	static function getCacheDriver()
	{
		return self::$cache;
	}
	/**
	 * @param Logger $logger
	 */
	static function setLogger($logger)
	{
		self::$logger = $logger;
	}
	/**
	 * @return Logger
	 */
	static function getLogger()
	{
		return self::$logger ?: new StdoutLogger();
	}
	/**
	 * Lookup value in cache
	 * @delegate to Cache->getStore()
	 * @param string $id
	 * @param Closure $retreiver
	 * @param string $tag
	 * @param int $lifetime
	 * @return mixed
	 */
	static function cacheLookup($id, $retreiver, $tag = null, $lifetime = null)
	{
		return self::$cache->getStore($id, $retreiver, $tag, $lifetime);
	}
	/**
	 * @return HttpResponse
	 */
	static function response()
	{
		if (null === self::$http_response)
		{
			self::$http_response = new HttpResponse();
		}
		return self::$http_response;
	}
	/**
	 * Share variable in a globally-available space
	 * @param string $key
	 * @param mixed $value
	 */
	static function put($key, $value)
	{
		self::$shared[$key] = $value;
	}
	/**
	 * get a shared variable.
	 * when the argument is not specified, the whole array of shared vars is returned.
	 * @param string $key
	 * @return mixed
	 */
	static function get($key = null, $default = null)
	{
		if (null === $key)
		{
			return self::$shared;
		} else {
			return array_key_exists($key, self::$shared) ? self::$shared[$key] : $default;
		}
	}
	/**
	 * @param array $map (LIBRARY-NAME => LIBRARY-ROOT-FOLDER)
	 */
	static function loadLibraryMap($map)
	{
		self::$library_map = array_merge(self::$library_map, $map);
	}
	/**
	 * @param string $root
	 */
	static function setDefaultLibraryRoot($root)
	{
		self::$default_library_root = $root;
	}
	/**
	 * Configure KM
	 * @param string | array $settings either a filename where a $settings array is defined or an array of settings
	 * @param string | array $sections which section of a file to load. Each subsequent section
	 * 	overrides previous
	 */
	static function configure($settings, $sections = null)
	{
		if (is_string($settings))
		{
			include $settings;
		}
		
		if (null === $sections)
		{
			self::$settings = array_merge(self::$settings, $settings);
		} else {
			$merge = array(self::$settings);
			foreach ((array) $sections as $section) {
				if (array_key_exists($section, $settings)) {
					$merge[] = $settings[$section];
				}
			}
			self::$settings = call_user_func_array('array_merge', $merge);
		}
		
	}
	/**
	 * Get a configuration entry or export teh whole configuration array
	 * @param string $key
	 * @return mixed
	 */
	static function config($key = null)
	{
		if (null === $key)
		{
			return self::$settings;
		} else {
			return self::$settings[$key];
		}
	}
	
	/**
	 * Throw 404, 403 or 401 Exception (will we need others?)
	 */
	static function fail($code, $message = null)
	{
		switch ($code) {
			case 404:
				throw new km\exception\NotFoundException($message);
				break;
			case 403:
				throw new km\exception\ForbiddenException($message);
				break;
			case 401:
				throw new km\exception\UnauthorizedException($message);
				break;
			case 400:
				throw new km\exception\BadRequestException();
				break;
			default:
				throw new InvalidArgumentException('km::fail() : code is not recognized');
				break;
		}
	}
	
	/**
	 * Register a "standalone" class
	 * @param string $class
	 * @param string $filename
	 */
	static function registerClass($class, $filename)
	{
		self::$class_map[$class] = $filename;
	}
	/**
	 * Set exception handlers
	 */
	static function setExceptionHandlers($handlers)
	{
		self::$exception_handlers = $handlers;
	}
	static function getExceptionHandlers()
	{
		return self::$exception_handlers;
	}
	/**
	 * Start throwing exceptions on every error
	 */
	static function errors2exceptions()
	{
		set_error_handler(function($errno, $errstr) {
			if ($errno & error_reporting()) throw new km\exception\PhpErrorException($errstr);
		});
	}
	/**
	 * Start exception handling the Km way
	 */
	static function catchExceptions()
	{
		set_exception_handler(array(__CLASS__, 'exceptionHandler'));
	}
	static function exceptionHandler($exception)
	{
		try {
			$class = get_class($exception);
			if (array_key_exists($class, self::$exception_handlers))
			{
				$handler = self::$exception_handlers[$class];
			} else {
				foreach (self::$exception_handlers as $exception_class => $handler) {
					if ($exception instanceof $exception_class) {
						break;
					}
				}
			}

			if (! isset($handler))
			{
				die('Cannot handle exception of a type ' . $class);
			}

			self::create($handler)->handle($exception);
		} catch (\Exception $e) {
			self::getLogger()->write(get_class($e) . ' thrown within exception handler: ' . $e->getMessage() .
				". Trace: " . $e->getTraceAsString());
			exit;
		}
	}
	/**
	 * start autoloading
	 */
	static function registerAutoload()
	{
		spl_autoload_register(array(__CLASS__, 'autoload'));
	}
	
	/**
	 * Autoload
	 *
	 * @param string $class
	 * @return Closure
	 */
	static function autoload($class)
	{
		$class_normalized = str_replace(array('::', '\\'), '_', $class);
		if (false === ($p = strpos($class_normalized, '_')))
		{
			$libraryName = $class_normalized;
		} else {
			$libraryName = substr($class_normalized, 0, $p);
		}
		
		if (array_key_exists($class, self::$class_map)) {
			include_once self::$class_map[$class];
		} elseif (array_key_exists($libraryName, self::$library_map)) {
			include_once self::$library_map[$libraryName] . "/" . str_replace('_', '/', $class_normalized) . '.php';
		} elseif (self::$default_library_root) {
			include_once self::$default_library_root . "/" . str_replace('_', '/', $class_normalized) . '.php';
		} else {
			throw new LogicException('Class ' . $class . ' could not be found');
		}
	}
	/**
	 * @param Closure | callable $callable
	 * @return Closure
	 * @throws InvalidArgumentException
	 */
	static function toClosure($callable)
	{
		if ($callable instanceof Closure)
		{
			return $callable;
		} elseif (@is_callable($callable)) {
			return function () use ($callable) {
				$args = func_get_args();
				return call_user_func_array($callable, $args);
			};
		} else {
			throw new InvalidArgumentException();
		}
	}
}